package com.nightpiter.controller.algorithms;

import static com.nightpiter.controller.log.Logger.log;
import static com.nightpiter.controller.log.Logger.logProfilerBegin;
import static com.nightpiter.controller.log.Logger.logProfilerEnd;
import android.os.Handler;

import com.nightpiter.controller.log.Logger;
import com.nightpiter.exceptions.StationNotFoundDijkstraException;
import com.nightpiter.model.MetroModel;
import com.nightpiter.model.MetroModelState;
import com.nightpiter.model.objects.Station;
import com.nightpiter.model.objects.Transition;
import com.nightpiter.controller.algorithms.MetroRouterState.Action;

/**
 * Dijkstra's shortest path algorithm
 * 
 * @author Eldar Abusalimov (eldar.abusalimov@gmail.com)
 */
public class MetroRouter {

	/**
	 * Router instance prepared to work with the model
	 */
	private static MetroRouter instance = null;

	/**
	 * the state
	 */
	MetroRouterState state;

	/**
	 * graph to search
	 */
	private Graph graph;

	/**
	 * vertex from which begins search
	 */
	private Vertex fromVertex;

	/**
	 * sorted list implementation instance to store not yet visited vertices
	 */
	private SortedVertexList unvisiteds;

	/**
	 * sorted list to store the next pass unvisited vertices
	 */
	private SortedVertexList nextPassUnvisiteds;

	/**
	 * @return MetroRouter instance prepared to work with the current model
	 */
	public static MetroRouter getInstance() {
		return instance;
	}

	/**
	 * private constructor
	 * 
	 * @param model
	 */
	private MetroRouter(MetroModel model) {
		this.graph = new Graph(model);
		this.state = new MetroRouterState();
	}

	/**
	 * This method performs searching of the shortest ways between stationFrom
	 * and all others by using Dijkstra's algorithm
	 * 
	 * @param fromStation
	 *            the station from which begins search
	 */
	public void prepare(Station fromStation, Handler handler) {
		
		if (fromStation == null) {
			log("fromStation == null");
			this.state.action = Action.IDLE;
			state.setError(new StationNotFoundDijkstraException());
			return;
		}
		if (fromVertex != null && fromVertex.getStation() == fromStation) {
			log("fromVertex.getStation() == fromStation");
			this.state.action = Action.IDLE;
			this.state.setReady(true);
			return;
		}

		this.state.action = Action.PREPARING;
		this.state.resetAll(handler);

		nextPassUnvisiteds = new SortedVertexList(0);

		logProfilerBegin("Starting new Dijkstra's calculation");
		logProfilerBegin("reset each vertex");
		this.graph.reset();
		logProfilerEnd("reset each vertex");

		fromVertex = graph.getVerticesHash().get(fromStation);
		if (fromVertex != null) {
			fromVertex.setAsStart();
			nextPassUnvisiteds.add(fromVertex);
			log("Vertex for stationFrom (" + fromStation.getName() + ") found");
		} else {
			this.state.action = Action.IDLE;
			state.setError(new StationNotFoundDijkstraException(fromStation));
			logProfilerEnd("Dijkstra's algorithm exited with an error");
			return;
		}

		log("Starting algorithm");

		logProfilerBegin("executing main loop");
		do {
			unvisiteds = nextPassUnvisiteds;
			nextPassUnvisiteds = new SortedVertexList(unvisiteds.getPass() + 1);
			logProfilerBegin("pass " + unvisiteds.getPass());
			log("before: thisPassUnvisiteds size: " + unvisiteds.size());
			log("before: nextPassUnvisiteds size: " + nextPassUnvisiteds.size());
			logProfilerBegin("processing vertices");

			this.state.setPass(unvisiteds.getPass());
			while (!unvisiteds.isEmpty()) {
				processVertex(unvisiteds.getFirst());
			}

			logProfilerEnd("processing vertices");
			log("after:  thisPassUnvisiteds size: " + unvisiteds.size());
			log("after:  nextPassUnvisiteds size: " + nextPassUnvisiteds.size());
			logProfilerEnd("pass");
		} while (!nextPassUnvisiteds.isEmpty());
		logProfilerEnd("main loop");

		this.state.action = Action.IDLE;
		this.state.setReady(true);
		logProfilerEnd("Dijkstra's algorithm successfully completed");
	}

	/**
	 * get all possible routes to the station
	 * 
	 * @param toStation
	 * @return routes array
	 * @throws StationNotFoundDijkstraException
	 *             if the presented station does not belong to the model
	 */
	public Route[] getRoutes(Station toStation)
			throws StationNotFoundDijkstraException {

		Vertex toVertex = graph.getVerticesHash().get(toStation);
		if (toVertex != null) {
			log("Vertex for stationTo (" + toStation.getName() + ") found");
		} else {
			log("presented station does not belong to the model: "
					+ toStation.getName());
			throw new StationNotFoundDijkstraException(toStation);
		}

		log("total ways: " + toVertex.passWayMap.size());

		Route[] routes = new Route[toVertex.passWayMap.size()];
		int i = 0;
		for (Way way : toVertex.passWayMap.values()) {
			routes[i++] = new Route(way, fromVertex, toVertex);
		}

		return routes;
	}

	/**
	 * executes the main step of Dijkstra's algorithm
	 * 
	 * Update distance and position in sorted list on those vertices which can
	 * be immediately reached from the current vertex. Mark the current vertex
	 * as visited.
	 * 
	 * @param vertex
	 *            the vertex to process
	 */
	private void processVertex(Vertex vertex) {
		logProfilerBegin("processing vertex (total ways: "
				+ vertex.passWayMap.size() + ", station " + vertex.getStation()
				+ ")");
		for (Edge edge : vertex.getEdges()) {
			tryEdge(edge);
		}
		vertex.setAsVisited(unvisiteds.getPass());
		unvisiteds.remove(vertex);
		logProfilerEnd("removing vertex from unvisiteds (new size: "
				+ unvisiteds.size() + ")");
	}

	/**
	 * tests the edge to be the shortest way to the vertex
	 * 
	 * @param edge
	 *            the edge to test
	 */
	private void tryEdge(Edge edge) {
		Vertex neighbor = edge.getToVertex();
		int pass = unvisiteds.getPass();
		if (neighbor.isVisited(pass)) {
			return;
		}
		Transition transition = edge.getTransition();
		log("found unvisited neighbor (" + neighbor.getStation().getName()
				+ "); reach time: " + transition.getTime());
		try {
			if (neighbor.hasWayEdges()) {
				if (neighbor.tryWay(edge, unvisiteds, nextPassUnvisiteds)) {
					log("|-new way accepted (total ways: "
							+ neighbor.passWayMap.size() + ")");
				} else {
					log("|-leaving (total ways: " + neighbor.passWayMap.size()
							+ ")");
				}
			} else {
				if (neighbor.tryWay(edge, unvisiteds, nextPassUnvisiteds)) {
					log("|-new way accepted (total ways: "
							+ neighbor.passWayMap.size() + ")");
				} else {
					throw new InternalDijkstraException("a strange situation..");
				}
			}
		} catch (InternalDijkstraException e) {
			Logger.logException(Logger.ERROR, e);
		}
	}

	/**
	 * @return the state
	 */
	public MetroRouterState getState() {
		return state;
	}

	public static void onModelStateChanged(MetroModel model, MetroModelState state) {
		MetroModelState currentState = model.getState();
		if (state.getAction() != MetroModelState.Action.IDLE) {
			instance = null;
		} else if (currentState.isReady()) {
			// TODO unexpected behavior if state is too out-of-date
			instance = new MetroRouter(model);
		}

	}

}
